﻿using System;
using RaytracerLib.MathLib;
using YAXLib;
using System.Diagnostics;
using TomanuExtensions;
using RaytracerLib.Rays;
using RaytracerLib.UVMappers;

namespace RaytracerLib.RenderableObjects
{
    public class SphereObject : RenderableObject
    {
        private double m_radius;

        public SphereObject(Vector3 a_right, Vector3 a_up) :
            base(a_right, a_up)
        {
            Name = "Sphere";
            Closed = true;
            m_uv_mapper = new SphericalUVMapper();
            Update(UpdateFlags.All);
        }

        [YAXNode]
        public double Radius
        {
            get
            {
                return m_radius;
            }
            set
            {
                m_radius = value;
                Update(UpdateFlags.BoundBox | UpdateFlags.Matrices);
            }
        }

        protected override Vector3 LocalScale
        {
            get
            {
                return new Vector3(Radius, Radius, Radius);
            }
        }

        protected override AABB GetLocalBoundBox()
        {
            return new AABB(-Vector3.ONE, Vector3.ONE);
        }

        internal override Intersection GetIntersection(Intersection a_source_ray_intersection, Ray a_ray)
        {
            Vector3 local_dir;
            Vector3 local_start;

            if (!TransformToLocalToBoundBox(a_ray, out local_dir, out local_start))
                return Scene.NoIntersection;

            bool back_hit = false;
            double dist;

            if ((a_source_ray_intersection != null) && a_source_ray_intersection.SceneObject == this)
            {
                Debug.Assert((a_ray.RaySurfaceSide == RaySurfaceSide.RefractedSide) ||
                                (a_ray.RaySurfaceSide == RaySurfaceSide.ReflectedSide));
                if (a_source_ray_intersection.BackHit ^ (a_ray.RaySurfaceSide == RaySurfaceSide.ReflectedSide))
                    return Scene.NoIntersection;
                else
                {
                    if (OneSide)
                        return Scene.NoIntersection;
                    else
                    {
                        back_hit = true;
                        double v = -(local_start * local_dir);
                        double m2 = 1 - (local_start.SqrLen - v * v);

                        if (m2.IsAlmostGreaterOrEqualThen(0))
                        {
                            if (m2 < 0)
                                m2 = 0;
                        }
                        else
                            return Scene.NoIntersection;

                        dist = v + Math.Sqrt(m2);

                        if (dist <= 0)
                            return Scene.NoIntersection;
                    }
                }
            }
            else
            {
                double c2 = local_start.SqrLen;

                if (c2 > 1)
                {
                    double v = -(local_start * local_dir);

                    if (v <= 0)
                        return Scene.NoIntersection;
                    else
                    {
                        double m2 = 1 - (c2 - v * v);

                        if (m2.IsAlmostGreaterOrEqualThen(0))
                        {
                            if (m2 < 0)
                                m2 = 0;
                            if (m2.IsAlmostLessOrEqualThen(1))
                            {
                                if (m2 > 1)
                                    m2 = 1;
                            }
                            else
                                return Scene.NoIntersection;
                        }
                        else
                            return Scene.NoIntersection;

                        dist = v - Math.Sqrt(m2);
                    }
                }
                else
                {
                    back_hit = true;

                    if (OneSide)
                        return Scene.NoIntersection;
                    else
                    {
                        double v = -(local_start * local_dir);
                        double m2 = 1 - (c2 - v * v);

                        if (m2.IsAlmostGreaterOrEqualThen(0))
                        {
                            if (m2 < 0)
                                m2 = 0;
                        }
                        else
                            return Scene.NoIntersection;

                        dist = v + Math.Sqrt(m2);
                    }
                }
            }

            Vector3 local_pos = local_start + local_dir * dist;
            Vector3 world_pos = LocalToWorld * local_pos;
            double world_dist = (world_pos - a_ray.Start).Length;

            Intersection intersection = new Intersection()
            {
                PrevIntersection = a_source_ray_intersection,
                SceneObject = this,
                SourceRay = a_ray,
                Dist = world_dist,
                Scene = Scene,
                BackHit = back_hit,
                Pos = world_pos,
                LocalPos = local_pos
            };

            // Numerical errors.
            if (intersection.Normal * intersection.SourceRay.Dir >= 0)
                return Scene.NoIntersection;

            return intersection;
        }

        internal override Vector3 GetNormal(Intersection a_intersection)
        {
            if (a_intersection.BackHit)
                return -(LocalToWorldNormal * a_intersection.LocalPos).Normalized;
            else
                return (LocalToWorldNormal * a_intersection.LocalPos).Normalized;
        }

        internal override Vector3 GetUVW(Intersection a_intersection)
        {
            return base.GetUVW(a_intersection) * 0.5 + Vector3.HALF;
        }

        public override string ToString()
        {
            return String.Format("Sphere: {0}, R: {1}, Pos: {2}", Name, Radius, Pos);
        }

        internal override void GetTangents(Intersection a_intersection, out Vector3 a_tangent_x,
            out Vector3 a_tangent_y)
        {
            if (a_intersection.BackHit)
            {
                a_tangent_x = Vector3.CrossProduct(
                    Up, a_intersection.Normal).Normalized;
                a_tangent_y = Vector3.CrossProduct(
                    a_tangent_x, a_intersection.Normal).Normalized;
            }
            else
            {
                a_tangent_x = Vector3.CrossProduct(
                    a_intersection.Normal, Up).Normalized;
                a_tangent_y = Vector3.CrossProduct(
                    a_intersection.Normal, a_tangent_x).Normalized;
            }
        }

        public override void ScaleAbsolute(double a_scale)
        {
            Radius *= a_scale;

            base.ScaleAbsolute(a_scale);
        }
    }
}